/* * Copyright 2016 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.diffplug.common.base; import java.lang.reflect.Constructor; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.function.BiPredicate; import java.util.function.Function; /** A mechanism for comparing trees. */ public final class TreeComparison<E, A> { /** The tree we expected to get. */ private final TreeDef<E> expectedDef; private final E expectedRoot; /** The tree we actually got. */ private final TreeDef<A> actualDef; private final A actualRoot; /** Functions for decorating the two sides of the tree when generating ComparisonFailures. */ private Function<? super E, String> expectedToString = Object::toString; private Function<? super A, String> actualToString = Object::toString; private TreeComparison(TreeDef<E> expectedDef, E expectedRoot, TreeDef<A> actualDef, A actualRoot) { this.expectedDef = expectedDef; this.expectedRoot = expectedRoot; this.actualDef = actualDef; this.actualRoot = actualRoot; } /** Returns true if the two trees are equal, by calling {@link Objects#equals(Object, Object)} on the results of both mappers. */ public boolean isEqualMappedBy(Function<? super E, ?> expectedMapper, Function<? super A, ?> actualMapper) { return isEqualBasedOn((expected, actual) -> { return Objects.equals(expectedMapper.apply(expected), actualMapper.apply(actual)); }); } /** Returns true if the two trees are equal, based on the given {@link BiPredicate}. */ public boolean isEqualBasedOn(BiPredicate<? super E, ? super A> compareFunc) { return equals(expectedDef, expectedRoot, actualDef, actualRoot, compareFunc); } /** Recursively determines equality between two trees. */ private static <E, A> boolean equals(TreeDef<E> expectedDef, E expectedRoot, TreeDef<A> actualDef, A actualRoot, BiPredicate<? super E, ? super A> compareFunc) { // compare the roots if (!compareFunc.test(expectedRoot, actualRoot)) { return false; } // compare the children lists List<E> expectedChildren = expectedDef.childrenOf(expectedRoot); List<A> actualChildren = actualDef.childrenOf(actualRoot); if (expectedChildren.size() != actualChildren.size()) { return false; } // recurse on each pair of children for (int i = 0; i < expectedChildren.size(); ++i) { E expectedChild = expectedChildren.get(i); A actualChild = actualChildren.get(i); if (!equals(expectedDef, expectedChild, actualDef, actualChild, compareFunc)) { return false; } } return true; } /** * Asserts that the trees are equal, by calling {@link Objects#equals(Object, Object)} on the results of both mappers. * * @see #isEqualMappedBy(Function, Function) */ public void assertEqualMappedBy(Function<? super E, ?> expectedMapper, Function<? super A, ?> actualMapper) { if (!isEqualMappedBy(expectedMapper, actualMapper)) { throwAssertionError(); } } /** * Asserts that the trees are equal, based on the given {@link BiPredicate}. * * @see #isEqualBasedOn(BiPredicate) */ public void assertEqualBasedOn(BiPredicate<E, A> compareFunc) { if (!isEqualBasedOn(compareFunc)) { throwAssertionError(); } } /** Decorates errors thrown by any assertions with the given functions. */ public TreeComparison<E, A> decorateErrorsWith(Function<? super E, String> expectedToString, Function<? super A, String> actualToString) { this.expectedToString = expectedToString; this.actualToString = actualToString; return this; } /** Throws an assetion error. */ private void throwAssertionError() { throw createAssertionError(); } /** * Returns an {@link AssertionError} containing the contents of the * two trees. Attempts to throw a JUnit ComparisonFailure if JUnit * is on the class path, but it fails to a plain old {@code java.lang.AssertionError} * if the reflection calls fail. */ private AssertionError createAssertionError() { // convert both sides to strings String expected = TreeQuery.toString(expectedDef, expectedRoot, expectedToString); String actual = TreeQuery.toString(actualDef, actualRoot, actualToString); // try to create a junit ComparisonFailure for (String exceptionType : Arrays.asList( "org.junit.ComparisonFailure", "junit.framework.ComparisonFailure")) { try { return createComparisonFailure(exceptionType, expected, actual); } catch (Exception e) {} } // we'll have to settle for a plain-jane AssertionError return new AssertionError("Expected:\n" + expected + "\n\nActual:\n" + actual); } /** Attempts to create an instance of junit's ComparisonFailure exception using reflection. */ private AssertionError createComparisonFailure(String className, String expected, String actual) throws Exception { Class<?> clazz = Class.forName(className); Constructor<?> constructor = clazz.getConstructor(String.class, String.class, String.class); return (AssertionError) constructor.newInstance("", expected, actual); } /** Maps both sides of the comparison to the same type, for easier comparison and assertions. */ public <T> SameType<T> mapToSame(Function<? super E, ? extends T> mapExpected, Function<? super A, ? extends T> mapActual) { return new SameTypeImp<>(this, mapExpected, mapActual); } /** An API for comparing trees which have been mapped to the same type. */ public interface SameType<T> { /** Returns true if the trees are equal. */ boolean isEqual(); /** Asserts that the trees are equal. */ void assertEqual(); /** Decorates errors thrown by any assertions with the given functions. */ SameType<T> decorateErrorsWith(Function<? super T, String> toString); /** Maps this SameType to some other type. */ <R> SameType<R> map(Function<? super T, ? extends R> mapper); } /** A TreeComparison with convenience methods for creating a new */ private static class SameTypeImp<E, A, T> implements SameType<T> { private final TreeComparison<E, A> comparison; private final Function<? super E, ? extends T> mapExpected; private final Function<? super A, ? extends T> mapActual; public SameTypeImp(TreeComparison<E, A> comparison, Function<? super E, ? extends T> mapExpected, Function<? super A, ? extends T> mapActual) { this.comparison = comparison; this.mapExpected = mapExpected; this.mapActual = mapActual; comparison.decorateErrorsWith(mapExpected.andThen(Objects::toString), mapActual.andThen(Objects::toString)); } /** Returns true if the two trees are equal. */ @Override public boolean isEqual() { return comparison.isEqualMappedBy(mapExpected, mapActual); } /** Asserts that the two trees are equal. */ @Override public void assertEqual() { comparison.assertEqualMappedBy(mapExpected, mapActual); } /** Decorates errors thrown by assertions with the given function. */ @Override public SameType<T> decorateErrorsWith(Function<? super T, String> toString) { comparison.decorateErrorsWith(toString.compose(mapExpected), toString.compose(mapActual)); return this; } /** Maps the variable on which the comparisons will take place. */ @Override public <R> SameType<R> map(Function<? super T, ? extends R> mapper) { return new SameTypeImp<>(comparison, mapExpected.andThen(mapper), mapActual.andThen(mapper)); } } /** Creates a {@link TreeComparison} for comparing the two trees. */ public static <E, A> TreeComparison<E, A> of(TreeDef<E> expectedDef, E expectedRoot, TreeDef<A> actualDef, A actualRoot) { return new TreeComparison<>(expectedDef, expectedRoot, actualDef, actualRoot); } /** Creates a {@link SameType} for comparing two trees of the same type. */ public static <T> SameType<T> of(TreeDef<T> treeDef, T expected, T actual) { return of(treeDef, expected, treeDef, actual).mapToSame(Function.identity(), Function.identity()); } /** Creates a {@link SameType} for comparing a {@link TreeNode} against a generic tree. */ public static <T> SameType<T> of(TreeNode<T> expected, TreeDef<T> treeDef, T actual) { return of(expected, treeDef, actual, Function.identity()); } /** Creates a {@link SameType} for comparing a {@link TreeNode} against a generic tree which been mapped. */ public static <T, U> SameType<T> of(TreeNode<T> expected, TreeDef<U> treeDef, U actual, Function<? super U, ? extends T> mapper) { return of(TreeNode.treeDef(), expected, treeDef, actual).mapToSame(TreeNode::getContent, mapper); } /** Creates a {@link SameType} from the given two {@link TreeNode}s of the same type. */ public static <T> SameType<T> of(TreeNode<T> expected, TreeNode<T> actual) { return of(TreeNode.treeDef(), expected, TreeNode.treeDef(), actual).mapToSame(TreeNode::getContent, TreeNode::getContent); } }